Skip to content

feat!: config consolidation for v1#661

Open
harlan-zw wants to merge 1 commit intomainfrom
feat/registry-config-redesign
Open

feat!: config consolidation for v1#661
harlan-zw wants to merge 1 commit intomainfrom
feat/registry-config-redesign

Conversation

@harlan-zw
Copy link
Collaborator

@harlan-zw harlan-zw commented Mar 20, 2026

Summary

Consolidates registry config, first-party mode defaults, and DX improvements for v1.

Config redesign: presence = infrastructure, trigger = load

  • Adding a script to the registry enables infrastructure (proxy routes, types, bundling, composable auto-imports) without auto-loading it
  • Scripts only auto-load globally when trigger is explicitly set
  • true and 'proxy-only' removed as config values (build errors with migration messages)
  • trigger, proxy, bundle, partytown hoisted to flat config syntax
scripts: {
  registry: {
    // Infrastructure only (composable driven)
    googleAnalytics: { id: 'G-XXXXXX' },
    // Infrastructure + global auto-load
    plausibleAnalytics: { domain: 'mysite.com', trigger: 'onNuxtReady' },
    // Opt out of proxy
    posthog: { proxy: false },
  }
}

reverseProxyIntercept renamed to proxy

Three clean single-word capabilities: bundle, proxy, partytown. Per-script opt-out is now { proxy: false } instead of { reverseProxyIntercept: false }.

Privacy defaults: PRIVACY_NONEPRIVACY_IP_ONLY

Every proxied script now anonymizes IP at minimum (/24 subnet, city-level geo accuracy). Three tiers:

  • Full: Ad pixels (Meta, TikTok, X, Snapchat, Reddit)
  • Heatmap-safe: GA, Clarity, Hotjar
  • IP only: Everything else (was previously "None")

Docs rewrite

  • Two-layer framing (routing vs anonymization)
  • Accurate script categorization: Full First-Party / Proxy Only / Bundle Only / Excluded
  • Flat config syntax in all examples

Breaking Changes

  • reverseProxyInterceptproxy (all source, types, config)
  • true removed as registry config value
  • 'proxy-only' removed as registry config value
  • Scripts without trigger no longer auto-load globally
  • IP anonymization now default for all proxied scripts (use privacy: false to restore raw forwarding)

Migration

Before After
googleAnalytics: true googleAnalytics: {} or googleAnalytics: { trigger: 'onNuxtReady' }
googleAnalytics: 'proxy-only' googleAnalytics: {}
{ reverseProxyIntercept: false } { proxy: false }
[{ id: '...' }, { reverseProxyIntercept: false }] { id: '...', proxy: false }

Test plan

  • All 862 unit tests pass
  • Build succeeds
  • Typecheck passes
  • E2E verification with playground

@vercel
Copy link
Contributor

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Mar 23, 2026 4:55am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 20, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@661

commit: e40f62e

@harlan-zw harlan-zw marked this pull request as ready for review March 20, 2026 13:26
@harlan-zw harlan-zw force-pushed the feat/registry-config-redesign branch from e1ee76a to 85c19a4 Compare March 20, 2026 13:28
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the first-party proxy configuration system across the codebase. The changes rename the reverseProxyIntercept capability to proxy, update registry entry normalization to reject boolean true and require explicit trigger configuration for composable auto-injection, restructure NuxtConfigScriptRegistryEntry type to support per-script trigger, proxy, bundle, and partytown options, and adjust privacy defaults for multiple registry entries from PRIVACY_NONE to PRIVACY_IP_ONLY. Template generation now conditionally emits composable calls only when trigger is present, treating entries without triggers as infrastructure-only. The changes span type definitions, normalization logic, template generation, first-party proxy handling, and comprehensive test and documentation updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main breaking change: a config consolidation redesign for v1 that changes how the registry configuration works.
Description check ✅ Passed The description comprehensively covers the redesign with clear sections on config changes, capability renaming, privacy defaults, breaking changes, migration guide, and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/registry-config-redesign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/fixtures/first-party/nuxt.config.ts (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Inconsistent vercelAnalytics value in runtimeConfig.

Line 38 in runtimeConfig.public.scripts has vercelAnalytics: true, but true is no longer a valid configuration value per this PR. This should be changed to vercelAnalytics: {} for consistency with the registry entry at line 76.

🔧 Proposed fix
-        vercelAnalytics: true,
+        vercelAnalytics: {},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/first-party/nuxt.config.ts` at line 38, Update the
runtimeConfig.public.scripts entry for vercelAnalytics to use an object instead
of a boolean: locate runtimeConfig.public.scripts and change the vercelAnalytics
value from true to an empty object (vercelAnalytics: {}) so it matches the
registry format used elsewhere (see the existing registry entry for
vercelAnalytics).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@test/fixtures/first-party/nuxt.config.ts`:
- Line 38: Update the runtimeConfig.public.scripts entry for vercelAnalytics to
use an object instead of a boolean: locate runtimeConfig.public.scripts and
change the vercelAnalytics value from true to an empty object (vercelAnalytics:
{}) so it matches the registry format used elsewhere (see the existing registry
entry for vercelAnalytics).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e8ac1e4-8ecb-45b1-96e0-69df230f37fd

📥 Commits

Reviewing files that changed from the base of the PR and between d8378ce and 85c19a4.

📒 Files selected for processing (9)
  • playground/nuxt.config.ts
  • src/module.ts
  • src/normalize.ts
  • src/runtime/types.ts
  • src/templates.ts
  • test/fixtures/basic/nuxt.config.ts
  • test/fixtures/first-party/nuxt.config.ts
  • test/unit/auto-inject.test.ts
  • test/unit/templates.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/assets.ts`:
- Line 56: The expression creating cleanPath uses (event.path ||
'').split('?')[0] which TypeScript flags as possibly undefined; update the
assignment in src/assets.ts (the cleanPath declaration) to provide a safe
fallback for the split result before calling .slice(1) — e.g. take the first
element of the split with a nullish/empty-string fallback (so the index access
cannot be undefined) or use destructuring with a default, ensuring the final
value passed to .slice is always a string derived from event.path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef88c5d3-176b-496b-80bb-fcce7b24621b

📥 Commits

Reviewing files that changed from the base of the PR and between 85c19a4 and acdad53.

📒 Files selected for processing (1)
  • src/assets.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/assets.ts (1)

56-56: ⚠️ Potential issue | 🔴 Critical

Fix the remaining TS2532 on cleanPath construction.

At Line 56, event.path! avoids only the nullable path case; split('?')[0] is still typed as possibly undefined with strict indexed access, so .slice(1) keeps typecheck failing.

🔧 Proposed fix
-        const cleanPath = event.path!.split('?')[0].slice(1)
+        const cleanPath = (event.path?.split('?')[0] ?? '').slice(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/assets.ts` at line 56, The TS2532 arises because event.path can be
undefined and indexed access split('?')[0] is still considered possibly
undefined; replace the non-null assertion with a safe default so the expression
always yields a string. Change the construction of cleanPath to use a
nullish-coalescing default on event.path (e.g., (event.path ?? '') or
String(event.path)) before calling split and slice so that cleanPath is
calculated from a guaranteed string; update the statement that sets cleanPath
accordingly (referencing cleanPath and event.path in your edit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/assets.ts`:
- Line 56: The TS2532 arises because event.path can be undefined and indexed
access split('?')[0] is still considered possibly undefined; replace the
non-null assertion with a safe default so the expression always yields a string.
Change the construction of cleanPath to use a nullish-coalescing default on
event.path (e.g., (event.path ?? '') or String(event.path)) before calling split
and slice so that cleanPath is calculated from a guaranteed string; update the
statement that sets cleanPath accordingly (referencing cleanPath and event.path
in your edit).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0406ca9-c121-4202-a16a-08fb1a5e8ca1

📥 Commits

Reviewing files that changed from the base of the PR and between acdad53 and 74c2afa.

📒 Files selected for processing (1)
  • src/assets.ts

- Rename `reverseProxyIntercept` to `proxy` across all source, tests, docs
- Change default privacy from `PRIVACY_NONE` to `PRIVACY_IP_ONLY` for 11 scripts
- Redesign registry config: presence = infrastructure, trigger = load
- Remove `true` and `'proxy-only'` as config values (build errors with migration messages)
- Hoist `trigger`, `proxy`, `bundle`, `partytown` to flat config syntax
- Rewrite first-party docs with two-layer framing and accurate script categorization
- Update templates to only generate composable calls when trigger is present

BREAKING CHANGE: `reverseProxyIntercept` renamed to `proxy`. Registry `true` and `'proxy-only'` removed. Scripts without `trigger` no longer auto-load globally.
@harlan-zw harlan-zw force-pushed the feat/registry-config-redesign branch from 74c2afa to e40f62e Compare March 23, 2026 04:53
@harlan-zw harlan-zw changed the title feat!: redesign registry config — presence = available, trigger = load feat!: config consolidation for v1 Mar 23, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/first-party/setup.ts (1)

76-84: ⚠️ Potential issue | 🟠 Major

Respect runtimeConfig.public.scripts[registryKey].proxy === false in auto-inject.

This helper already prefers the resolved runtime entry at Lines 79-84, but the opt-out guard runs before rtEntry is read. If proxying is disabled there, apiHost/endpoint still gets injected and flips the SDK back to the first-party URL. Please gate on the runtime entry too and add a regression test for that path.

Suggested fix
-  // Per-script proxy opt-out (in input or scriptOptions)
-  if (input?.proxy === false || scriptOptions?.proxy === false)
-    return
   const rtScripts = runtimeConfig.public?.scripts as Record<string, any> | undefined
   const rtEntry = rtScripts?.[registryKey]
+  // Per-script proxy opt-out (in input, scriptOptions, or runtimeConfig)
+  if (input?.proxy === false || scriptOptions?.proxy === false || rtEntry?.proxy === false)
+    return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/first-party/setup.ts` around lines 76 - 84, The early opt-out check only
inspects input?.proxy and scriptOptions?.proxy but ignores the resolved runtime
entry, so proxy:false in runtimeConfig.public.scripts[registryKey] is not
respected; update the guard to also check rtEntry?.proxy === false (ensure
rtEntry is read from runtimeConfig.public?.scripts[registryKey] before
returning) so that if the runtime entry disables proxy we skip injection, and
add a regression test that sets runtimeConfig.public.scripts[registryKey].proxy
= false and verifies apiHost/endpoint are not auto-injected.
src/plugins/transform.ts (1)

297-323: ⚠️ Potential issue | 🟠 Major

Honor config-level proxy: false before generating proxy rewrites.

firstPartyOptOut is derived only from literal call-site args, so useScriptFoo() still falls through to proxyRewrites when proxying was disabled in scripts.registry or runtimeConfig.public.scripts for that integration. Lines 297-323 already load the merged config for the script; use that as the default here and let any explicit call-site value override it. Otherwise the global opt-out is ineffective for bundled integrations.

Also applies to: 401-439

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/transform.ts` around lines 297 - 323, The bug is that
firstPartyOptOut is computed only from literal call-site args (fnArg0) so
registry-level proxy: false is ignored; fix by deriving the proxy opt-out from
the merged config (mergedOptions) with call-site values overriding it: after you
build mergedOptions = { ...registryConfig, ...fnArg0 } use mergedOptions.proxy
as the default when computing firstPartyOptOut (e.g., set firstPartyOptOut =
mergedOptions.proxy === false ? true : /*fall back to existing call-site
logic*/), and apply the same change in the other block noted (lines ~401-439) so
registry/runtimeConfig-level proxy:false prevents proxy rewrites unless
explicitly overridden by the call-site.
🧹 Nitpick comments (1)
test/unit/proxy-configs.test.ts (1)

429-462: Cover the rest of the PRIVACY_IP_ONLY migrations in this table.

ipOnly only includes a subset of the entries switched in src/registry.ts, so several changed defaults (matomoAnalytics, carbonAds, lemonSqueezy, gravatar, vimeoPlayer, youtubePlayer, etc.) now get boolean-shape checks only. Expanding this table—or deriving expected presets from the registry—would keep this migration from regressing silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/proxy-configs.test.ts` around lines 429 - 462, The ipOnly list in
the 'all configs have valid structure' test is missing several keys that were
moved into PRIVACY_IP_ONLY in src/registry.ts, causing those entries to be only
shape-checked; update the test to either (a) include the full set of keys that
live in PRIVACY_IP_ONLY (add matomoAnalytics, carbonAds, lemonSqueezy, gravatar,
vimeoPlayer, youtubePlayer, etc.) to the ipOnly array, or (b) import
PRIVACY_IP_ONLY from src/registry.ts and use that constant instead of the
hardcoded ipOnly array so the test automatically covers all migrated entries;
adjust the assertions that follow (expect(...).toBe(true / false) checks) to run
for that complete set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/1.guides/2.first-party.md`:
- Around line 24-41: The copy incorrectly states that triggerless registry
entries “are bundled”; update the wording in the scripts.registry explanatory
paragraph to avoid saying they are always bundled and instead say that any
supported infrastructure is prepared/registered (proxy routes, composables,
bundling when applicable) — edit the paragraph that mentions `trigger` and the
examples `googleAnalytics`, `metaPixel`, `plausibleAnalytics` to replace “is
bundled” with phrasing like “infrastructure is prepared/registered” and clarify
that bundling only happens for scripts that require it.

In `@src/normalize.ts`:
- Around line 53-65: The current merge applies legacy scriptOptions last and
thus overwrites hoisted top-level flags; change the merge so top-level values
win by combining legacy then top-level (e.g., set mergedScriptOptions =
Object.assign({}, scriptOptions, mergedScriptOptions)) or iterate scriptOptions
and only assign keys that are undefined in mergedScriptOptions; update the code
around scriptOptions / mergedScriptOptions (references: entry, scriptOptions,
mergedScriptOptions, SCRIPT_OPTION_KEYS, input) accordingly.

In `@src/runtime/types.ts`:
- Line 229: NuxtConfigScriptRegistryEntry's object branch creates an
uninhabitable intersection when T can be boolean literal true (e.g.
ScriptRegistry's carbonAds?: true), so change the object branch to first
normalize true to an empty object before intersecting; add a small helper type
(e.g. ReplaceTrueWithEmpty<T>) that maps true to {} and otherwise returns T,
then use ReplaceTrueWithEmpty<T> & { trigger?:
NuxtUseScriptOptionsSerializable['trigger'], proxy?: boolean, bundle?: boolean,
partytown?: boolean, scriptOptions?: Omit<NuxtUseScriptOptionsSerializable,
'trigger'> } (and keep the existing union members) so zero-input scripts like
true can be expressed as the object form {} or { trigger: ... } without
producing an impossible type.

---

Outside diff comments:
In `@src/first-party/setup.ts`:
- Around line 76-84: The early opt-out check only inspects input?.proxy and
scriptOptions?.proxy but ignores the resolved runtime entry, so proxy:false in
runtimeConfig.public.scripts[registryKey] is not respected; update the guard to
also check rtEntry?.proxy === false (ensure rtEntry is read from
runtimeConfig.public?.scripts[registryKey] before returning) so that if the
runtime entry disables proxy we skip injection, and add a regression test that
sets runtimeConfig.public.scripts[registryKey].proxy = false and verifies
apiHost/endpoint are not auto-injected.

In `@src/plugins/transform.ts`:
- Around line 297-323: The bug is that firstPartyOptOut is computed only from
literal call-site args (fnArg0) so registry-level proxy: false is ignored; fix
by deriving the proxy opt-out from the merged config (mergedOptions) with
call-site values overriding it: after you build mergedOptions = {
...registryConfig, ...fnArg0 } use mergedOptions.proxy as the default when
computing firstPartyOptOut (e.g., set firstPartyOptOut = mergedOptions.proxy ===
false ? true : /*fall back to existing call-site logic*/), and apply the same
change in the other block noted (lines ~401-439) so registry/runtimeConfig-level
proxy:false prevents proxy rewrites unless explicitly overridden by the
call-site.

---

Nitpick comments:
In `@test/unit/proxy-configs.test.ts`:
- Around line 429-462: The ipOnly list in the 'all configs have valid structure'
test is missing several keys that were moved into PRIVACY_IP_ONLY in
src/registry.ts, causing those entries to be only shape-checked; update the test
to either (a) include the full set of keys that live in PRIVACY_IP_ONLY (add
matomoAnalytics, carbonAds, lemonSqueezy, gravatar, vimeoPlayer, youtubePlayer,
etc.) to the ipOnly array, or (b) import PRIVACY_IP_ONLY from src/registry.ts
and use that constant instead of the hardcoded ipOnly array so the test
automatically covers all migrated entries; adjust the assertions that follow
(expect(...).toBe(true / false) checks) to run for that complete set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1298c65c-a10a-4325-948b-0e7e5e9456c7

📥 Commits

Reviewing files that changed from the base of the PR and between 74c2afa and e40f62e.

📒 Files selected for processing (26)
  • FIRST_PARTY.md
  • docs/content/docs/1.guides/2.first-party.md
  • docs/content/docs/3.api/5.nuxt-config.md
  • docs/content/docs/4.migration-guide/1.v0-to-v1.md
  • docs/content/docs/5.releases/1.v1.md
  • playground/nuxt.config.ts
  • src/first-party/proxy-configs.ts
  • src/first-party/resolve-capabilities.ts
  • src/first-party/setup.ts
  • src/module.ts
  • src/normalize.ts
  • src/plugins/rewrite-ast.ts
  • src/plugins/transform.ts
  • src/registry.ts
  • src/runtime/types.ts
  • src/stats.ts
  • src/templates.ts
  • test/fixtures/basic/nuxt.config.ts
  • test/fixtures/first-party/nuxt.config.ts
  • test/unit/auto-inject.test.ts
  • test/unit/first-party.test.ts
  • test/unit/normalize.test.ts
  • test/unit/proxy-configs.test.ts
  • test/unit/setup.test.ts
  • test/unit/templates.test.ts
  • test/unit/third-party-proxy-replacements.test.ts
✅ Files skipped from review due to trivial changes (5)
  • docs/content/docs/5.releases/1.v1.md
  • test/unit/third-party-proxy-replacements.test.ts
  • src/plugins/rewrite-ast.ts
  • docs/content/docs/4.migration-guide/1.v0-to-v1.md
  • test/fixtures/basic/nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/unit/auto-inject.test.ts
  • playground/nuxt.config.ts
  • test/fixtures/first-party/nuxt.config.ts
  • src/module.ts
  • test/unit/templates.test.ts

Comment on lines +24 to +41
First-party mode is **auto-enabled** for all scripts that support it. Adding a script to the registry enables infrastructure (proxy routes, bundling, types, composables) without auto-loading it:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
registry: {
// Infrastructure only — use composables to load on specific pages
googleAnalytics: { id: 'G-XXXXXX' },
metaPixel: { id: '123456' },

// Infrastructure + global auto-load
plausibleAnalytics: { domain: 'mysite.com', trigger: 'onNuxtReady' },
}
}
})
```

Each script in the registry declares its own capabilities. Scripts that support `reverseProxyIntercept` will automatically route collection requests through your domain. Scripts that support `bundle` will be downloaded at build time and served locally.
Scripts without `trigger` are infrastructure only: proxy routes are registered, the script is bundled, and composables are available, but the script only loads when you call the composable in a component. Add `trigger` to auto-load globally.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t describe triggerless entries as always bundled.

Line 41 says the script “is bundled”, but this page later calls out proxy-only cases like PostHog and Matomo. Rephrasing this to “any supported infrastructure is prepared” avoids contradicting those scripts.

✏️ Suggested wording
-Scripts without `trigger` are infrastructure only: proxy routes are registered, the script is bundled, and composables are available, but the script only loads when you call the composable in a component. Add `trigger` to auto-load globally.
+Scripts without `trigger` are infrastructure only: the module prepares any supported infrastructure for that script (for example proxy routes, bundling, and composables), but the script only loads when you call the composable in a component. Add `trigger` to auto-load globally.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
First-party mode is **auto-enabled** for all scripts that support it. Adding a script to the registry enables infrastructure (proxy routes, bundling, types, composables) without auto-loading it:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
registry: {
// Infrastructure only — use composables to load on specific pages
googleAnalytics: { id: 'G-XXXXXX' },
metaPixel: { id: '123456' },
// Infrastructure + global auto-load
plausibleAnalytics: { domain: 'mysite.com', trigger: 'onNuxtReady' },
}
}
})
```
Each script in the registry declares its own capabilities. Scripts that support `reverseProxyIntercept` will automatically route collection requests through your domain. Scripts that support `bundle` will be downloaded at build time and served locally.
Scripts without `trigger` are infrastructure only: proxy routes are registered, the script is bundled, and composables are available, but the script only loads when you call the composable in a component. Add `trigger` to auto-load globally.
First-party mode is **auto-enabled** for all scripts that support it. Adding a script to the registry enables infrastructure (proxy routes, bundling, types, composables) without auto-loading it:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/1.guides/2.first-party.md` around lines 24 - 41, The copy
incorrectly states that triggerless registry entries “are bundled”; update the
wording in the scripts.registry explanatory paragraph to avoid saying they are
always bundled and instead say that any supported infrastructure is
prepared/registered (proxy routes, composables, bundling when applicable) — edit
the paragraph that mentions `trigger` and the examples `googleAnalytics`,
`metaPixel`, `plausibleAnalytics` to replace “is bundled” with phrasing like
“infrastructure is prepared/registered” and clarify that bundling only happens
for scripts that require it.

Comment on lines +53 to +65
const { scriptOptions, ...rest } = entry
const input: Record<string, any> = {}
const mergedScriptOptions: Record<string, any> = {}

for (const [k, v] of Object.entries(rest)) {
if ((SCRIPT_OPTION_KEYS as readonly string[]).includes(k))
mergedScriptOptions[k] = v
else
input[k] = v
}

if (scriptOptions)
Object.assign(mergedScriptOptions, scriptOptions)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefer the new top-level flags when both shapes are present.

Line 64 currently merges legacy scriptOptions after the hoisted top-level fields, so a partial migration like { proxy: true, scriptOptions: { proxy: false } } normalizes to proxy: false. That makes the visible top-level value lie about the effective config.

🛠️ Suggested fix
       const { scriptOptions, ...rest } = entry
       const input: Record<string, any> = {}
       const mergedScriptOptions: Record<string, any> = {}

+      if (scriptOptions)
+        Object.assign(mergedScriptOptions, scriptOptions)
+
       for (const [k, v] of Object.entries(rest)) {
         if ((SCRIPT_OPTION_KEYS as readonly string[]).includes(k))
           mergedScriptOptions[k] = v
         else
           input[k] = v
       }
-
-      if (scriptOptions)
-        Object.assign(mergedScriptOptions, scriptOptions)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/normalize.ts` around lines 53 - 65, The current merge applies legacy
scriptOptions last and thus overwrites hoisted top-level flags; change the merge
so top-level values win by combining legacy then top-level (e.g., set
mergedScriptOptions = Object.assign({}, scriptOptions, mergedScriptOptions)) or
iterate scriptOptions and only assign keys that are undefined in
mergedScriptOptions; update the code around scriptOptions / mergedScriptOptions
(references: entry, scriptOptions, mergedScriptOptions, SCRIPT_OPTION_KEYS,
input) accordingly.

export type RegistryScriptKey = Exclude<keyof ScriptRegistry, `${string}-npm`>

export type NuxtConfigScriptRegistryEntry<T> = true | false | 'mock' | T | [T, NuxtUseScriptOptionsSerializable]
export type NuxtConfigScriptRegistryEntry<T> = false | 'mock' | (T & { trigger?: NuxtUseScriptOptionsSerializable['trigger'], proxy?: boolean, bundle?: boolean, partytown?: boolean, scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'> }) | [T, NuxtUseScriptOptionsSerializable]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "^\s+\w+\?: true$" src/runtime/types.ts -C1
rg -n "export type NuxtConfigScriptRegistryEntry<T>" src/runtime/types.ts -A8 -B1

Repository: nuxt/scripts

Length of output: 829


🏁 Script executed:

rg -n "^\s+\w+\?:\s*true\s*$" src/runtime/types.ts

Repository: nuxt/scripts

Length of output: 78


The new object form is not usable for zero-input scripts due to uninhabitable type intersection.

ScriptRegistry includes carbonAds?: true, and the current NuxtConfigScriptRegistryEntry<T> type creates an intersection true & { trigger?, proxy?, ... } which TypeScript cannot inhabit. This blocks the documented migration to object form ({} / { trigger: 'onNuxtReady' }) for zero-input scripts.

The fix requires a helper type to normalize true to {} before intersection:

Suggested type fix
+type RegistryConfigInput<T> = [T] extends [true] ? {} : T
+
-export type NuxtConfigScriptRegistryEntry<T> = false | 'mock' | (T & { trigger?: NuxtUseScriptOptionsSerializable['trigger'], proxy?: boolean, bundle?: boolean, partytown?: boolean, scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'> }) | [T, NuxtUseScriptOptionsSerializable]
+export type NuxtConfigScriptRegistryEntry<T>
+  = | false
+    | 'mock'
+    | (RegistryConfigInput<T> & {
+        trigger?: NuxtUseScriptOptionsSerializable['trigger']
+        proxy?: boolean
+        bundle?: boolean
+        partytown?: boolean
+        scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'>
+      })
+    | [RegistryConfigInput<T>, NuxtUseScriptOptionsSerializable]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type NuxtConfigScriptRegistryEntry<T> = false | 'mock' | (T & { trigger?: NuxtUseScriptOptionsSerializable['trigger'], proxy?: boolean, bundle?: boolean, partytown?: boolean, scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'> }) | [T, NuxtUseScriptOptionsSerializable]
type RegistryConfigInput<T> = [T] extends [true] ? {} : T
export type NuxtConfigScriptRegistryEntry<T>
= | false
| 'mock'
| (RegistryConfigInput<T> & {
trigger?: NuxtUseScriptOptionsSerializable['trigger']
proxy?: boolean
bundle?: boolean
partytown?: boolean
scriptOptions?: Omit<NuxtUseScriptOptionsSerializable, 'trigger'>
})
| [RegistryConfigInput<T>, NuxtUseScriptOptionsSerializable]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/types.ts` at line 229, NuxtConfigScriptRegistryEntry's object
branch creates an uninhabitable intersection when T can be boolean literal true
(e.g. ScriptRegistry's carbonAds?: true), so change the object branch to first
normalize true to an empty object before intersecting; add a small helper type
(e.g. ReplaceTrueWithEmpty<T>) that maps true to {} and otherwise returns T,
then use ReplaceTrueWithEmpty<T> & { trigger?:
NuxtUseScriptOptionsSerializable['trigger'], proxy?: boolean, bundle?: boolean,
partytown?: boolean, scriptOptions?: Omit<NuxtUseScriptOptionsSerializable,
'trigger'> } (and keep the existing union members) so zero-input scripts like
true can be expressed as the object form {} or { trigger: ... } without
producing an impossible type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant